This notebook is a step-by-step walkthrough to:
Both goals can be adapted for other purposes and applications. The applied mathematical equations and stoichiometry can be adapted to fit the process under investigation, while the the method and steps in the approach remain the same.
To run this code, the pyIDEAS package (Van Daele, 2015), needs to be installed. This code is publicly available at github.com/FILLINNAMEHERE
In [1]:
import pandas as pd
import numpy as np
import collections
In [2]:
import matplotlib.pyplot as plt
In [3]:
from pyideas import (Model,AlgebraicModel, Measurements, ParameterOptimisation,
CalibratedConfidence, ModPar)
In [4]:
%matplotlib inline
In [5]:
#Read total dataset - will be used in model calibration
conc_profile = pd.read_csv('ProcessedData_Example.csv')[[r'Experiment','EtOH', 'AA','BA', 'HA', 'mu']]
Data_All = conc_profile.set_index([r'Experiment', 'EtOH', 'AA','BA', 'HA'])
#Read dataset per substrate - will be used in model selection step
##AA
Data_AA = pd.read_csv('ProcessedData_Example.csv')[[r'Experiment','AA', 'mu']]
Data_AA_A = Data_AA[Data_AA.Experiment==1]
Data_AA_B = Data_AA[Data_AA.Experiment==2]
Data_AA = pd.concat([Data_AA_A, Data_AA_B])
Data_AA.columns=['Experiment','S', 'mu']
Data_AA = Data_AA.set_index([r'Experiment', 'S'])
Data_AA = Data_AA.sort_index(level='S')
##BA
Data_BA = pd.read_csv('ProcessedData_Example.csv')[[r'Experiment','AA','BA', 'mu']]
Data_BA_E = Data_BA[Data_BA.Experiment==5]
Data_BA_F = Data_BA[Data_BA.Experiment==6]
Data_BA = pd.concat([Data_BA_E, Data_BA_F])
Data_BA.columns=['Experiment','AA','S', 'mu']
Data_BA = Data_BA.set_index([r'Experiment', 'AA','S'])
Data_BA = Data_BA.sort_index(level=['AA','S'])
##HA
Data_HA = pd.read_csv('ProcessedData_Example.csv')[[r'Experiment','HA', 'mu']]
Data_HA = Data_HA[Data_HA.Experiment==7]
Data_HA.columns=['Experiment','S', 'mu']
Data_HA = Data_HA.set_index([r'Experiment', 'S'])
Data_HA = Data_HA.sort_index(level='S')
In [6]:
type(Data_AA)
Out[6]:
In [7]:
#Define kinetic AA-models for model selection
Syst_AA_Monod = {'mu': ('mu_max*(S/(K_S + S))')} # Monod, 1949
Syst_AA_Haldane= {'mu': ('mu_max*(S/(K_S+S+S^2/Ki))')} # Sivakumar et al., 1994
Syst_AA_Mon_ToxLim= {'mu': ('mu_max*(S/(K_S + S))*(1-1/(1+exp(-(S-Ki))))')} #Proposed
#Define initial parameter estimations for calibration of model selections
parameters_AA_Mon = {'K_S': 3.5, #Cavalcante et al. (2016)
'mu_max': max(Data_All.mu)} #Maximum of all obtained growth rates in experiments A and B
parameters_AA_Hal = {'mu_max': max(Data_All.mu),
'K_S': 3.5,
'Ki': 330.} #Arbitrarily chosen inhibition concentration
parameters_AA_Mon_ToxLim = {'mu_max': max(Data_All.mu),
'K_S': 3.5,
'Ki': 330.}
In [8]:
AA_Monod = AlgebraicModel('3D', Syst_AA_Monod, parameters_AA_Mon, ['S'])
AA_Monod_data = Measurements(Data_AA)
AA_Monod_data.add_measured_errors({'mu': 0.04}, method='absolute')
AA_Monod_optim = ParameterOptimisation(AA_Monod, AA_Monod_data,
optim_par=['mu_max', 'K_S'])
In [9]:
AA_Monod_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[9]:
The value 'fun' is the residuals after fitting of the model to the data. For the conventional Monod model, the residuals value is 566.6.
In [10]:
AA_Monod_conf = CalibratedConfidence(AA_Monod_optim)
AA_Monod_conf.get_parameter_confidence()
Out[10]:
In [11]:
AA_Monod_optim.modmeas.plot(style='o')
Out[11]:
In [12]:
AA_Haldane = AlgebraicModel('3D', Syst_AA_Haldane, parameters_AA_Hal, ['S'])
AA_Haldane_data = Measurements(Data_AA)
AA_Haldane_data.add_measured_errors({'mu': 0.04}, method='absolute')
AA_Haldane_optim = ParameterOptimisation(AA_Haldane, AA_Haldane_data,
optim_par=['mu_max', 'K_S','Ki'])
In [13]:
AA_Haldane_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
In [14]:
AA_Haldane.parameters
Out[14]:
In [15]:
AA_Haldane_conf = CalibratedConfidence(AA_Haldane_optim)
AA_Haldane_conf.get_parameter_confidence()
In [16]:
AA_Mon_ToxLim = AlgebraicModel('3D', Syst_AA_Mon_ToxLim, parameters_AA_Mon_ToxLim, ['S'])
AA_Mon_ToxLim_data = Measurements(Data_AA)
AA_Mon_ToxLim_data.add_measured_errors({'mu': 0.04}, method='absolute')
AA_Mon_ToxLim_optim = ParameterOptimisation(AA_Mon_ToxLim, AA_Mon_ToxLim_data,
optim_par=['mu_max', 'K_S'])
In [17]:
AA_Mon_ToxLim_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[17]:
For the Monod-model combined with a toxicity limit term, the residuals value drops to 27.3. Since Haldane did not obtain any fit, and the residuals value of the conventional Monod model was 566.6, this model is selected for further modelling of kinetics with growth on AA.
In [18]:
AA_Mon_ToxLim.parameters
Out[18]:
In [19]:
AA_Mon_ToxLim_conf = CalibratedConfidence(AA_Mon_ToxLim_optim)
AA_Mon_ToxLim_conf.get_parameter_confidence()
Out[19]:
In [20]:
AA_Mon_ToxLim_optim.modmeas.plot(style='o')
Out[20]:
In [21]:
#Define kinetic BA-models for model selection
Syst_BA_ToxLim= {'mu': ('mu_max*(1-1/(1+exp(-(S-Ki))))*(AA/(K_AA + AA))*(1-1/(1+exp(-(AA-Ki_AA))))')} #Proposed in this study
Syst_BA_LinInh= {'mu': ('mu_max*(1-K*S)*(1-1/(1+exp(-(S-1/K))))*(AA/(K_AA + AA))*(1-1/(1+exp(-(AA-Ki_AA))))')} # Hinshelwood (1952)
#Define initial parameter estimations for calibration of model selections
parameters_BA_ToxLim = {'mu_max': max(Data_All.mu),
'Ki':150., # Estimated based on available data
'K_AA':4.7, #Affinity index for AA obtained from model selection performed above
'Ki_AA':330.}
parameters_BA_LinInh = {'mu_max': max(Data_All.mu),
'K':1/150,
'K_AA':4.7,
'Ki_AA':330.}
In [22]:
BA_ToxLim = AlgebraicModel('3D', Syst_BA_ToxLim, parameters_BA_ToxLim, ['S','AA'])
BA_ToxLim_data = Measurements(Data_BA)
BA_ToxLim_data.add_measured_errors({'mu': 0.04}, method='absolute')
BA_ToxLim_optim = ParameterOptimisation(BA_ToxLim, BA_ToxLim_data,
optim_par=['mu_max', 'Ki'])
In [23]:
BA_ToxLim_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[23]:
In [24]:
BA_ToxLim.parameters
Out[24]:
In [25]:
BA_ToxLim_conf = CalibratedConfidence(BA_ToxLim_optim)
BA_ToxLim_conf.get_parameter_confidence()
Out[25]:
In [26]:
BA_ToxLim_optim.modmeas.plot(style='o')
Out[26]:
In [27]:
BA_LinInh = AlgebraicModel('3D', Syst_BA_LinInh, parameters_BA_LinInh, ['S','AA'])
BA_LinInh_data = Measurements(Data_BA)
BA_LinInh_data.add_measured_errors({'mu': 0.04}, method='absolute')
BA_LinInh_optim = ParameterOptimisation(BA_LinInh, BA_LinInh_data,
optim_par=['mu_max', 'K'])
In [28]:
BA_LinInh_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[28]:
Using a toxicity limit term, a residuals value of 39.8 was obtained, while with a linear inhibition term, this was 42.2. Based on this, the toxicity limit term was selected for further kinetic models.
In [29]:
BA_LinInh.parameters
Out[29]:
In [30]:
BA_LinInh_conf = CalibratedConfidence(BA_LinInh_optim)
BA_LinInh_conf.get_parameter_confidence()
Out[30]:
In [31]:
BA_LinInh_optim.modmeas.plot(style='o')
Out[31]:
In [32]:
Syst_HA_ToxLim= {'mu': ('mu_max*(1-1/(1+exp(-(S-Ki))))')} # This study
Syst_HA_LinInh= {'mu': ('mu_max*(1-K*S)*(1-1/(1+exp(-(S-1/K))))')} # Hinshelwood (1952)
parameters_HA_ToxLim = {'mu_max': max(Data_All.mu),
'Ki':90.}
parameters_HA_LinInh = {'mu_max': max(Data_All.mu),
'K':1/90}
In [33]:
HA_ToxLim = AlgebraicModel('3D', Syst_HA_ToxLim, parameters_HA_ToxLim, ['S'])
HA_ToxLim_data = Measurements(Data_HA)
HA_ToxLim_data.add_measured_errors({'mu': 0.04}, method='absolute')
HA_ToxLim_optim = ParameterOptimisation(HA_ToxLim, HA_ToxLim_data,
optim_par=['mu_max', 'Ki'])
In [34]:
HA_ToxLim_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[34]:
In [35]:
HA_ToxLim.parameters
Out[35]:
In [36]:
HA_ToxLim_conf = CalibratedConfidence(HA_ToxLim_optim)
HA_ToxLim_conf.get_parameter_confidence()
Out[36]:
In [37]:
HA_ToxLim_optim.modmeas.plot(style='o')
Out[37]:
In [38]:
HA_LinInh = AlgebraicModel('3D', Syst_HA_LinInh, parameters_HA_LinInh, ['S'])
HA_LinInh_data = Measurements(Data_HA)
HA_LinInh_data.add_measured_errors({'mu': 0.04}, method='absolute')
HA_LinInh_optim = ParameterOptimisation(HA_LinInh, HA_LinInh_data,
optim_par=['mu_max', 'K'])
In [39]:
HA_LinInh_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[39]:
Comparing the residuals value of 2.8 obtained with the linear inhibition model to the 11.0 obtained by a toxicity limit model - as well as the visually better fit - gives a clear preference to the linear inhibition model as the better model.
In [40]:
HA_LinInh.parameters
Out[40]:
In [41]:
HA_LinInh_conf = CalibratedConfidence(HA_LinInh_optim)
HA_LinInh_conf.get_parameter_confidence()
Out[41]:
In [42]:
HA_LinInh_optim.modmeas.plot(style='o')
Out[42]:
In [43]:
Syst_Total = {'mu': ('mu_max*(AA/(Ks_AA + AA))*(1-1/(1+exp(-(AA-Ki_AA))))*'
'(1-1/(1+exp(-(BA-Ki_BA))))*'
'(1-K_HA*HA)*(1-1/(1+exp(-(HA-1/K_HA))))')} #Multiplication of all terms selected above.
#Initial parameter estimates come from values obtained in model selection calibration
parameters_Total = {'mu_max': max(AA_Mon_ToxLim.parameters['mu_max'],BA_ToxLim.parameters['mu_max'],HA_LinInh.parameters['mu_max']),
'Ks_AA': AA_Mon_ToxLim.parameters['K_S'],
'Ki_AA':AA_Mon_ToxLim.parameters['Ki'],
'Ki_BA':BA_ToxLim.parameters['Ki'],
'K_HA':HA_LinInh.parameters['K']}
In [44]:
parameters_Total
Out[44]:
In [45]:
Total = AlgebraicModel('3D', Syst_Total, parameters_Total, ['AA','BA','HA'])
Total_data = Measurements(Data_All)
Total_data.add_measured_errors({'mu': 0.04}, method='absolute')
Total_optim = ParameterOptimisation(Total, Total_data,
optim_par=['mu_max','Ks_AA', 'Ki_BA','K_HA'])
In [46]:
Total_optim.local_optimize(method='Nelder-Mead',
options={'xatol': 1e-10, 'fatol': 1e-10})
Out[46]:
In [47]:
Total.parameters
Out[47]:
In [48]:
Total_conf = CalibratedConfidence(Total_optim)
In [49]:
Total_conf.get_parameter_confidence()
Out[49]:
In [50]:
Total_conf.get_parameter_correlation()
Out[50]:
In [51]:
Total_optim.modmeas.plot(style='o')
Out[51]:
In [52]:
Dynamic_System = {'dSEtOH': '(-(W1*mu_1/Y_EtOH) - (W2*mu_2/Y_EtOH))*X',
'dSAA': '(-2/3*(W1*mu_1/Y_EtOH)+1/6*(W2*mu_2/Y_EtOH))*X',
'dSBA': '(5/6*(W1*mu_1/Y_EtOH) - 5/6*(W2*mu_2/Y_EtOH))*X',
'dSHA': '5/6*(W2*mu_2/Y_EtOH)*X',
'dH2': '(2/6*(W1*mu_1/Y_EtOH) + 2/6*(W2*mu_2/Y_EtOH))*X',
'dH': '(1/6*(W1*mu_1/Y_EtOH) + 1/6*(W2*mu_2/Y_EtOH))*X',
'dX': 'mu*X',
'mu': 'W1*mu_1+W2*mu_2-b',
'W1':'(mu_1)/(mu_1+mu_2)',
'W2':'(mu_2)/(mu_1+mu_2)',
'mu_1': 'mu_max*(SAA/(Ks_AA + SAA))*(1-1/(1+exp(-(SAA-Ki_AA))))'
'*(1-1/(1+exp(-(SBA-Ki_BA))))'
'*(1-K_HA*SHA)*(1-1/(1+exp(-(SHA-1/K_HA))))'
'*(SEtOH/(Ks_EtOH + SEtOH))',
'mu_2': 'mu_max*(1-1/(1+exp(-(SBA-Ki_BA))))*(SBA/(Ks_BA + SBA))'
'*(1-K_HA*SHA)*(1-1/(1+exp(-(SHA-1/K_HA))))'
'*(1-1/(1+exp(-(SAA-Ki_AA))))'
'*(SEtOH/(Ks_EtOH + SEtOH))',
}
Dynamic_Par = {'Ks_AA': Total.parameters['Ks_AA'], #[mM]
'Ks_BA': 3.5, #[mM] -> Cavalcante et al. (2017)
'Ks_EtOH': 11.8, #[mM] Cavalcante et al. (2017)
'K_HA': Total.parameters['K_HA'], #[mM] -> From parameter estimation
'Ki_AA': Total.parameters['Ki_AA'], #[mM] -> From parameter estimation
'Ki_BA': Total.parameters['Ki_AA'], #[mM] -> From parameter estimation
'mu_max': Total.parameters['mu_max'], #[h^-1] -> From parameter estimation
'b': 0., #[h^-1] -> Can be added to the model, but parameter was neglected for this study
'Y_EtOH':2.75/1000, #[g Cells/mmol EtOH]
}
In [53]:
Dynamic_Init = {'SEtOH': 330, #[mM] #DSM52 composition
'SAA': 110, #[mM]
'SBA': 0, #[mM]
'SHA': 0, #[mM]
'H2': 0., #[mM]
'H': 1e-7, #[M]
'X': 0.13} #[g cells/L]
# Declaration of independent variable
time=np.linspace(0, 70, 50000)
independent = {'t': time}
# Run simulation
Dynamic = Model('Dynamic Simulation', Dynamic_System, Dynamic_Par)
Dynamic.parameters
Dynamic.initial_conditions = Dynamic_Init
Dynamic.independent = independent
Dynamic.initialize_model()
Dynamic_Out = Dynamic.run()
In [54]:
#Process ouput
X=Dynamic_Out['X']
SEtOH=Dynamic_Out['SEtOH']
SEtOH=pd.Series(SEtOH,index=time)
SAA=Dynamic_Out['SAA']
SAA=pd.Series(SAA,index=time)
SBA=Dynamic_Out['SBA']
SBA=pd.Series(SBA,index=time)
SHA=Dynamic_Out['SHA']
SHA=pd.Series(SHA,index=time)
In [55]:
Fig = plt.figure(figsize=((11,7*1.7)))
#Plot experiment data
a_X = plt.subplot(5,1,1)
a_X.set_title("Biomass",fontsize=14)
a_X.plot(X)
a_X.xaxis.set_visible(False)
a_X.locator_params(axis='y',nbins=3)
a_EtOH = plt.subplot(5,1,2)
a_EtOH.set_title("EtOH",fontsize=14)
a_EtOH.plot(SEtOH)
a_EtOH.xaxis.set_visible(False)
a_EtOH.locator_params(axis='y',nbins=4)
a_AA = plt.subplot(5,1,3)
a_AA.set_title("AA",fontsize=14)
a_AA.plot(SAA)
a_AA.xaxis.set_visible(False)
a_AA.locator_params(axis='y',nbins=3)
a_BA = plt.subplot(5,1,4)
a_BA.set_title("BA",fontsize=14)
a_BA.plot(SBA)
a_BA.xaxis.set_visible(False)
a_BA.locator_params(axis='y',nbins=3)
a_HA=plt.subplot(5,1,5)
a_HA.set_title("HA",fontsize=14)
a_HA.plot(SHA)
a_HA.locator_params(axis='y',nbins=3)
plt.xlabel('Time (h)', fontsize = 14)
plt.savefig('DynamicOutput.png',bbox_inches='tight')
In [56]:
Continuous_System = {'dSEtOH': '(-(W1*mu_1/Y_EtOH) - (W2*mu_2/Y_EtOH))*X+D*(SEtOH_in-SEtOH)',
'dSAA': '(-2/3*(W1*mu_1/Y_EtOH)+1/6*(W2*mu_2/Y_EtOH))*X+D*(SAA_in-SAA)',
'dSBA': '(5/6*(W1*mu_1/Y_EtOH) - 5/6*(W2*mu_2/Y_EtOH))*X+D*(SBA_in-SBA)',
'dSHA': '5/6*(W2*mu_2/Y_EtOH)*X+D*(SHA_in-SHA)',
'dH2': '(2/6*(W1*mu_1/Y_EtOH) + 2/6*(W2*mu_2/Y_EtOH))*X',
'dH': '(1/6*(W1*mu_1/Y_EtOH) + 1/6*(W2*mu_2/Y_EtOH))*X',
'dX': '(mu-D)*X',
'mu': 'W1*mu_1+W2*mu_2-b',
'W1':'(mu_1)/(mu_1+mu_2)',
'W2':'(mu_2)/(mu_1+mu_2)',
'mu_1': 'mu_max*(SAA/(Ks_AA + SAA))*(1-1/(1+exp(-(SAA-Ki_AA))))'
'*(1-1/(1+exp(-(SBA-Ki_BA))))'
'*(1-K_HA*SHA)*(1-1/(1+exp(-(SHA-1/K_HA))))'
'*(SEtOH/(Ks_EtOH + SEtOH))',
'mu_2': 'mu_max*(1-1/(1+exp(-(SBA-Ki_BA))))*(SBA/(Ks_BA + SBA))'
'*(1-K_HA*SHA)*(1-1/(1+exp(-(SHA-1/K_HA))))'
'*(1-1/(1+exp(-(SAA-Ki_AA))))'
'*(SEtOH/(Ks_EtOH + SEtOH))',
}
Continuous_Par = {'Ks_AA': Total.parameters['Ks_AA'], #[mM]
'Ks_BA': 3.5, #[mM] -> Cavalcante et al. (2017)
'Ks_EtOH': 11.8, #[mM] Cavalcante et al. (2017)
'K_HA': Total.parameters['K_HA'], #[mM] -> From parameter estimation
'Ki_AA': Total.parameters['Ki_AA'], #[mM] -> From parameter estimation
'Ki_BA': Total.parameters['Ki_AA'], #[mM] -> From parameter estimation
'mu_max': Total.parameters['mu_max'], #[h^-1] -> From parameter estimation
'b': 0., #[h^-1] -> Can be added to the model, but parameter was neglected for this study
'Y_EtOH':2.75/1000, #[g Cells/mmol EtOH]
# Parameters for chemostat operation - influent concentrations and dilution rate
'SEtOH_in':330,
'SAA_in':100,
'SBA_in':0.,
'SHA_in':0.,
'D':0.05,
}
In [57]:
Continuous_Init = {'SEtOH': Continuous_Par['SEtOH_in'], #[mM]
'SAA': Continuous_Par['SAA_in'], #[mM]
'SBA': Continuous_Par['SBA_in'], #[mM]
'SHA': Continuous_Par['SHA_in'], #[mM]
'H2': 0., #[mM]
'H': 1e-7, #[M]
'X': 0.13} #[g cells/L]
#Declaration of independent variable
time=np.linspace(0, 500, 5000000)
independent = {'t': time}
Continuous = Model('Continuous Simulation', Continuous_System, Continuous_Par)
Continuous.parameters
Continuous.initial_conditions = Continuous_Init
Continuous.independent = independent
Continuous.initialize_model()
Continuous_Out = Continuous.run()
In [58]:
X_Cont=Continuous_Out['X']
SEtOH_Cont=Continuous_Out['SEtOH']
SEtOH_Cont=pd.Series(SEtOH_Cont,index=time)
SAA_Cont=Continuous_Out['SAA']
SAA_Cont=pd.Series(SAA_Cont,index=time)
SBA_Cont=Continuous_Out['SBA']
SBA_Cont=pd.Series(SBA_Cont,index=time)
SHA_Cont=Continuous_Out['SHA']
SHA_Cont=pd.Series(SHA_Cont,index=time)
In [59]:
Fig_Cont = plt.figure(figsize=((11,7*1.7)))
#Plot experiment data
a_X = plt.subplot(5,1,1)
a_X.set_title("Biomass",fontsize=14)
a_X.plot(X_Cont)
a_X.xaxis.set_visible(False)
a_X.locator_params(axis='y',nbins=3)
a_EtOH = plt.subplot(5,1,2)
a_EtOH.set_title("EtOH",fontsize=14)
a_EtOH.plot(SEtOH_Cont)
a_EtOH.xaxis.set_visible(False)
a_EtOH.locator_params(axis='y',nbins=4)
a_AA = plt.subplot(5,1,3)
a_AA.set_title("AA",fontsize=14)
a_AA.plot(SAA_Cont)
a_AA.xaxis.set_visible(False)
a_AA.locator_params(axis='y',nbins=3)
a_BA = plt.subplot(5,1,4)
a_BA.set_title("BA",fontsize=14)
a_BA.plot(SBA_Cont)
a_BA.xaxis.set_visible(False)
a_BA.locator_params(axis='y',nbins=3)
a_HA=plt.subplot(5,1,5)
a_HA.set_title("HA",fontsize=14)
a_HA.plot(SHA_Cont)
a_HA.locator_params(axis='y',nbins=3)
plt.xlabel('Time (h)')
Out[59]:
Cavalcante WDA, Leitão RC, Gehring TA, Angenent LT, Santaella ST. 2017. Anaerobic fermentation for n-caproic acid production: A review. Process Biochem. 54:106–119
Daele T, Van Hoey S, Nopens I. 2015. pyIDEAS: an Open Source Python Package for 514 Model Analysis. In: . Comput Aided Chem Eng, Vol. 37, pp. 569–574.
Hinshelwood CN. 1952. The Chemical Kinetics Of The Bacterial Cell. Clarendon, Oxford.
Monod J. 1949. The Growth Of Bacterial Cultures. Annu. Rev. Microbiol. 3:371–394.
Sivakumar A, Srinivasaraghavan T, Swaminathan T, Baradarajan A. 1994. Extended monod kinetics for substrate inhibited systems. Bioprocess Eng. 11:185–188.